import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.HexFormat;


@RestController
public class WebhookController {

    private static final long WEBHOOK_TIMESTAMP_THRESHOLD_SECONDS = 30L;
    private static final String HMAC_SHA256 = "HmacSHA256";
    private static final String SIGNATURE_PREFIX = "v1=";
    private static final int READ_BUFFER_SIZE = 1024;


    @Value("${app.webhook.signature-key}")
    private String webhookSignatureKey;


    @PostMapping("/webhook")
    public ResponseEntity<String> handleWebhook(
        @RequestHeader(name = "Webhook-Timestamp") String timestampHeader,
        @RequestHeader(name = "Webhook-Signature") String signatureHeader,
        HttpServletRequest request
    ) throws IOException {

        // make sure you are using body buffering or read body another way
        try (var bodyInputStream = request.getInputStream()) {
            verifySignature(timestampHeader, signatureHeader, bodyInputStream);
        } catch (SecurityException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage());
        }

        // handle request

        return ResponseEntity.ok().build();
    }


    /**
     * Verifies signature of webhook/callback request and throws {@link SecurityException} if signature is not valid.
     *
     * @param timestampHeader Webhook-Timestamp header value, not null
     * @param signatureHeader Webhook-Signature header value, not null
     * @param body raw body input stream, not null. The stream will be fully read. The method does not close it.
     * @throws SecurityException if signature is not valid
     * @throws IOException if body reading issue happen
     */
    private void verifySignature(String timestampHeader, String signatureHeader, InputStream body) throws IOException {

        long timestamp;
        try {
            timestamp = Long.parseLong(timestampHeader);
        } catch (NumberFormatException e) {
            throw new SecurityException("Webhook-Timestamp has invalid format", e);
        }
        if (Instant.now().getEpochSecond() - timestamp > WEBHOOK_TIMESTAMP_THRESHOLD_SECONDS) {
            throw new SecurityException("Webhook-Timestamp exceed lifetime threshold");
        }

        Mac mac;
        try {
            mac = Mac.getInstance(HMAC_SHA256);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        SecretKeySpec secretKeySpec =
                new SecretKeySpec(webhookSignatureKey.getBytes(StandardCharsets.UTF_8), HMAC_SHA256);
        try {
            mac.init(secretKeySpec);
        } catch (InvalidKeyException e) {
            throw new RuntimeException(e);
        }

        mac.update(timestampHeader.getBytes(StandardCharsets.UTF_8));
        mac.update((byte) '.');
        byte[] buffer = new byte[READ_BUFFER_SIZE];
        int read;
        while ((read = body.read(buffer)) >= 0) {
            mac.update(buffer, 0, read);
        }
        byte[] computedSignature = mac.doFinal();

        for (var signatureRecord : signatureHeader.split(",")) {
            if (!signatureRecord.startsWith(SIGNATURE_PREFIX)) {
                continue;
            }
            byte[] signatureBytes;
            try {
                signatureBytes = HexFormat.of()
                        .parseHex(signatureRecord, SIGNATURE_PREFIX.length(), signatureRecord.length());
            } catch (IllegalArgumentException e) {
                // logger.debug("Cannot parse v1 signature: {}, skipping", signatureRecord, e);
                continue;
            }
            if (MessageDigest.isEqual(computedSignature, signatureBytes)) {
                return; // signature is correct
            }
        }
        throw new SecurityException("Invalid signature");
    }

}